//go:build windows
// +build windows

package veapplib

import (
	"bufio"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/StackExchange/wmi"
	gowin32wx "github.com/gorpher/gowin32"
	golnk "github.com/parsiya/golnk"
	uuid "github.com/satori/go.uuid"
	"github.com/shirou/gopsutil/v3/cpu"
	"github.com/shirou/gopsutil/v3/mem"
	psnet "github.com/shirou/gopsutil/v3/net"
	"github.com/urfave/cli/v2"
	"golang.org/x/sys/windows/registry"
	"image/png"
	"io/fs"
	"miao-client-plugins/utils/cmdutil"
	"net"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
)

// wmi 查询分析器 wbemtest
const (
	win32QueryForSYSAbstract  = "select  Name,PartOfDomain,SystemType,Domain  from Win32_ComputerSystem"
	win32QueryForSYSOperating = "select BuildNumber,Caption,OSArchitecture,SerialNumber,SystemDrive,Version from Win32_OperatingSystem"
	win32QueryForCPU          = "SELECT Name,NumberOfCores,NumberOfLogicalProcessors FROM Win32_Processor"
	// Win32_PhysicalMemory 适用的win版本范围非常有限 ：
	// Server 2012 R2、 8.1、 Server 2012、 8、 Server 2008 R2、win 7、 Server 2008 和  Vista： Server 2016和 win 10.
	win32QueryForMemory    = "SELECT Capacity,Speed FROM Win32_PhysicalMemory"
	win32QueryForLogicDisk = "SELECT DeviceID,FileSystem,DriveType,Size,FreeSpace FROM Win32_LogicalDisk"

	regQueryForRdpPort = "SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp"
)

type hostServicePlugin struct {
}

func (p hostServicePlugin) SayHi() {
	fmt.Println("host service plugin say hi")
}

func NewHostServicePlugin() hostServicePlugin {
	return hostServicePlugin{}
}

var (
	BuildTime string
	Version   = "1.0.0"
)

// Do the plugin
func Do() {
	app := cli.NewApp()
	hsp := NewHostServicePlugin()
	app.Name = "veapp-host"
	app.Version = fmt.Sprintf("%s build:%s", Version, BuildTime)
	app.Usage = "为veapp 对应的 host-service 提供数据支持"
	app.Description = "可以提供的功能包括：获取主机基础注册信息、获取特定路径下的快捷方式指向的应用信息、主机实时负载信息"
	app.Flags = []cli.Flag{
		&cli.BoolFlag{
			Name:    "info",
			Aliases: []string{"i"},
			Usage:   "获取主机基础注册信息",
			Action: func(ctx *cli.Context, b bool) error {
				info, err := hsp.baseInfo()
				if err != nil {
					return cli.Exit(fmt.Sprintf("获取主机基础注册信息时发送错误:%s", err.Error()), 1)
				}
				bs, err := json.Marshal(info)
				if err != nil {
					return cli.Exit(fmt.Sprintf("获取主机基础注册信息时发送错误:%s", err.Error()), 1)
				}
				w := bufio.NewWriter(os.Stdout)
				w.WriteString(string(bs) + "\n")
				w.Flush()
				return nil
			},
		},
		&cli.StringFlag{
			Name:    "apps",
			Aliases: []string{"a"},
			Usage:   "获取特定路径下的应用信息",
			Value:   "",
			Action: func(ctx *cli.Context, s string) error {
				iconPath := ctx.String("apps")
				if iconPath == "" {
					return cli.Exit(fmt.Sprintf("路径不能为空"), 1)
				}
				apps, err := hsp.appList(iconPath)
				if err != nil {
					return cli.Exit(fmt.Sprintf("获取云应用信息时发生错误：%s", err.Error()), 1)
				}
				bs, err := json.Marshal(apps)
				if err != nil {
					return cli.Exit(fmt.Sprintf("获取云应用信息时发生错误：%s", err.Error()), 1)
				}
				w := bufio.NewWriter(os.Stdout)
				w.WriteString(string(bs) + "\n")
				w.Flush()
				return nil
			},
		},
		&cli.BoolFlag{
			Name:  "load",
			Usage: "获取主机负载信息",
			Action: func(ctx *cli.Context, b bool) error {
				cpuPer, memPer, err := hsp.realTimeInfo()
				if err != nil {
					return cli.Exit(fmt.Sprintf("获取主机负载信息时发送错误:%s", err.Error()), 1)
				}
				ls := LoadInfo{
					Cpu: cpuPer,
					Mem: memPer,
				}
				bs, err := json.Marshal(ls)
				if err != nil {
					return cli.Exit(fmt.Sprintf("获取主机负载信息时发送错误:%s", err.Error()), 1)
				}
				w := bufio.NewWriter(os.Stdout)
				w.WriteString(string(bs) + "\n")
				w.Flush()
				return nil
			},
		},
		&cli.StringFlag{
			Name:    "getUser",
			Aliases: []string{"gu"},
			Usage:   "获取特定账户组下的账户列表,格式为：-gu=用户组",
			Value:   "",
			Action: func(ctx *cli.Context, s string) error {
				ud := NewUserDll()
				r, err := ud.GetGroupUsers(s)
				if err != nil {
					return errors.New(fmt.Sprintf("获取特定账户组下的账户列表时发生错误：%s", err.Error()))
				}
				w := bufio.NewWriter(os.Stdout)
				w.WriteString(r + "\n")
				w.Flush()
				return nil
			},
		},
		&cli.StringFlag{
			Name:    "addUser",
			Aliases: []string{"au"},
			Usage:   "添加用户,格式为：-au=账户/密码",
			Value:   "",
			Action: func(ctx *cli.Context, s string) error {
				ud := NewUserDll()
				in := strings.Split(s, "/")
				if len(in) != 2 {
					return cli.Exit(fmt.Sprintf("请按格式输入账号密码"), 1)
				}
				code, err := ud.AddUser(in[0], in[1])
				if err != nil {
					return cli.Exit(fmt.Sprintf("添加win账户时发生错误：%s,code:%d,账户：%s,密码：%s", err.Error(), code, in[0], in[1]), 1)
				}
				return nil
			},
		},
		&cli.StringFlag{
			Name:    "addUserToGroup",
			Aliases: []string{"autg"},
			Usage:   "添加用户到用户组,格式为：-autg=账户/组",
			Value:   "",
			Action: func(ctx *cli.Context, s string) error {
				ud := NewUserDll()
				in := strings.Split(s, "/")
				if len(in) != 2 {
					return cli.Exit(fmt.Sprintf("请按格式输入账号和组"), 1)
				}
				code, err := ud.AddUserToGroup(in[0], in[1])
				if err != nil {
					return cli.Exit(fmt.Sprintf("添加win账户时发生错误：%s,code:%d,账户：%s,组：%s", err.Error(), code, in[0], in[1]), 1)
				}
				return nil
			},
		},
		&cli.StringFlag{
			Name:    "batchCreateUsers",
			Aliases: []string{"bcu"},
			Usage:   "批量创建用户并将用户添加到远程桌面用户组,同时在E：\\user\\ 下创建对应的用户目录，格式为：-bcu=account1,account2 ",
			Value:   "",
			Action: func(ctx *cli.Context, s string) error {
				err := batchCreateUsers(s)
				if err != nil {
					return cli.Exit(fmt.Sprintf("批量创建rdp账户时发错误:%s", err), 1)
				}
				return nil
			},
		},
		&cli.StringFlag{
			Name:    "createUserFolder",
			Aliases: []string{"cuf"},
			Usage:   "创建用户目录，格式为：-cuf=目录路径/账户，该目录只有该用户才有完整权限，其他用户都没有完全访问权限.",
			Value:   "",
			Action: func(ctx *cli.Context, s string) error {
				in := strings.Split(s, "/")
				if len(in) != 2 {
					return cli.Exit(fmt.Sprintf("请按格式输入目录和账户"), 1)
				}
				_ = os.MkdirAll(in[0], 777)
				_, _, _, err := cmdutil.RunCommandArgs([]string{"Cacls", in[0], "/E", "/G", fmt.Sprintf("%s:F", in[1])}, cmdutil.CommandOption{})
				if err != nil {
					return cli.Exit(fmt.Sprintf("调整目录权限时发生错误:%s", err), 1)
				}
				return nil
			},
		},
		&cli.StringFlag{
			Name:    "mappingDir",
			Aliases: []string{"md"},
			Usage:   "映射目录为盘符，格式为：-md=目录路径/盘符名称.",
			Value:   "",
			Action: func(ctx *cli.Context, s string) error {
				in := strings.Split(s, "/")
				if len(in) != 2 {
					return cli.Exit(fmt.Sprintf("请按格式输入目录路径和盘符"), 1)
				}
				// subst 挂盘命令
				_, _, _, err := cmdutil.RunCommandArgs([]string{"subst", fmt.Sprintf("%s:", in[1]), in[0]}, cmdutil.CommandOption{})
				if err != nil {
					return cli.Exit(fmt.Sprintf("映射目录为盘符时发生错误:%s", err), 1)
				}
				return nil
			},
		},
		&cli.Int64Flag{
			Name:    "logoffSession",
			Aliases: []string{"ls"},
			Usage:   "注销会话,格式为：-ls=win 会话ID",
			Action: func(ctx *cli.Context, id int64) error {
				//sd := NewSessionDll()
				//return sd.Logoff(int32(id))
				_, _, _, err := cmdutil.RunCommandArgs([]string{"logoff", fmt.Sprintf("%d", id)}, cmdutil.CommandOption{})
				if err != nil {
					return cli.Exit(fmt.Sprintf("注销会话时发生错误:%s", err), 1)
				}
				return nil
			},
		},
	}
	app.Action = func(ctx *cli.Context) error {
		// 空实现，不然程序会默认打印 help 信息
		return nil
	}

	if err := app.Run(os.Args); err != nil {
		fmt.Printf("命令行调用错误：%s ，请查看帮助文档", err.Error())
	}
}

// baseInfo 获取主机基础信息
func (p hostServicePlugin) baseInfo() (info AppHost, err error) {
	var (
		s1      []SYSAbstract
		s2      []SYSOperating
		s3      []CpuInfo
		s4      []MemoryBaseInfo
		s5      []LogicDiskInfo
		ip      string
		mac     string
		rdpPort uint
	)
	if err = wmi.Query(win32QueryForSYSAbstract, &s1); err != nil {
		return
	}
	if err = wmi.Query(win32QueryForSYSOperating, &s2); err != nil {
		return
	}
	if err = wmi.Query(win32QueryForCPU, &s3); err != nil {
		return
	}

	if err = wmi.Query(win32QueryForMemory, &s4); err != nil {
		return
	}
	if err = wmi.Query(win32QueryForLogicDisk, &s5); err != nil {
		return
	}
	if len(s1) != len(s2) && len(s1) != 1 {
		err = errors.New("暂未适配多系统的场景")
		return
	}
	if len(s3) < 1 {
		err = errors.New("获取cpu信息失败")
		return
	}
	if len(s4) < 1 {
		s4 = make([]MemoryBaseInfo, 1)
		s4[0] = MemoryBaseInfo{
			Capacity: getMemorySize(),
		}
	}
	if len(s5) < 1 {
		err = errors.New("获取逻辑卷信息失败")
		return
	}
	if ip, mac, err = getIpMac(); err != nil {
		return
	}
	if rdpPort, err = getRdpPort(); err != nil {
		return
	}
	info = AppHost{
		HostName:       s1[0].Name,
		Ip:             ip,
		Mac:            mac,
		Domain:         s1[0].Domain,
		Os:             runtime.GOOS,
		OsCaption:      s2[0].Caption,
		PartOfDomain:   s1[0].PartOfDomain,
		SystemType:     s1[0].SystemType,
		BuildNumber:    s2[0].BuildNumber,
		OsArchitecture: s2[0].OSArchitecture,
		Version:        s2[0].Version,
		SerialNumber:   s2[0].SerialNumber,
		RdpPort:        uint8(rdpPort),
		CpuInfo:        strings.TrimSpace(s3[0].Name),
	}
	return
}

// appList 获取应用列表
func (p hostServicePlugin) appList(linkPath string) (li []AppInfo, err error) {
	li = make([]AppInfo, 0)
	err = filepath.Walk(linkPath, func(fp string, info fs.FileInfo, err error) error {
		if strings.Trim(path.Ext(path.Base(fp)), ".") == "lnk" {
			var appInfo AppInfo
			appInfo, err = getAppInfoByLnk(fp)
			if err == nil {
				li = append(li, appInfo)
			}
		}
		return nil
	})
	return li, nil
}

// realTimeInfo 获取主机实时负载信息，保持CPU、内存的使用百分比
func (p hostServicePlugin) realTimeInfo() (cpuPer, memPer float64, err error) {
	cs, err := cpu.Percent(1, false)
	if err != nil {
		return
	}
	if len(cs) < 1 {
		err = errors.New("获取CPU信息时发生错误")
		return
	}
	cpuPer = cs[0]
	vs, err := mem.VirtualMemory()
	if err != nil {
		return
	}
	memPer = vs.UsedPercent
	return
}

func getMemorySize() uint64 {
	vs, err := mem.VirtualMemory()
	if err != nil {
		return 0
	}
	return vs.Total
}

func getAppInfoByLnk(lnkPath string) (info AppInfo, err error) {
	var (
		flnk golnk.LnkFile
	)
	flnk, err = golnk.File(lnkPath)
	if err != nil {
		err = errors.New(fmt.Sprintf("无效的lnk path:%s", lnkPath))
		return
	}
	var iconStr string
	if strings.HasSuffix(flnk.StringData.IconLocation, ".ico") {
		iconStr, err = icoToBase64(flnk.StringData.IconLocation)
	} else {
		iconStr = getExeIconBase64(flnk.LinkInfo.LocalBasePath)
	}
	linkName := filepath.Base(lnkPath)
	appName := strings.TrimSuffix(linkName, filepath.Ext(linkName))
	info = AppInfo{
		AppName:   appName,
		LinkPath:  lnkPath,
		ExePath:   flnk.LinkInfo.LocalBasePath,
		AppParams: flnk.StringData.CommandLineArguments,
		WorkSpace: flnk.StringData.WorkingDir,
		Icon:      iconStr,
		Remark:    flnk.StringData.NameString,
	}
	return
}

// Deprecated: getExeIconBase64 获取icon图标，
// todo 这个是乌峡修改过的，特殊情况下会有问题，而且自动采集的图标存在大小显示问题，
// 苹果会将icon切分为16分，然后按比例拉伸来解决这个问题。我们可能不会做那么复杂，但是后续可能还是需要调整一下这段逻辑
func getExeIconBase64(execPath string) string {
	img, err := gowin32wx.ExtractPrivateExtractIcons(execPath, 128, 128)
	if err != nil {
		err = errors.New(fmt.Sprintf("gowin32wx.ExtractPrivateExtractIcons err:%s", err))
		return ""
	}
	fp, err := os.Create("output.png")
	defer func() {
		fp.Close()
		os.Remove("output.png")
	}()
	if err != nil {
		return ""
	}
	err = png.Encode(fp, img)
	if err != nil {
		return ""
	}
	b, err := os.ReadFile("output.png")
	if err != nil {
		return ""
	}
	imgStr := "data:image/png;base64," + base64.StdEncoding.EncodeToString(b)
	err = fp.Close()
	if err != nil {
		return ""
	}
	return imgStr
}

func icoToBase64(iconPath string) (str string, err error) {
	var (
		f *os.File
	)
	f, err = os.Open(iconPath)
	if err != nil {
		return
	}
	defer f.Close()
	// 读取ICO文件头
	var header struct {
		Reserved uint16
		Type     uint16
		Count    uint16
	}
	if err = binary.Read(f, binary.LittleEndian, &header); err != nil {
		err = errors.New(fmt.Sprintf("无法读取ICO文件头:%s", err))
		return
	}
	// 读取图标目录
	dirEntries := make([]struct {
		Width    uint8
		Height   uint8
		Colors   uint8
		Reserved uint8
		Planes   uint16
		BitCount uint16
		Size     uint32
		Offset   uint32
	}, header.Count)
	if err = binary.Read(f, binary.LittleEndian, &dirEntries); err != nil {
		err = errors.New(fmt.Sprintf("无法读取ICO图标目录:%s", err))
		return
	}
	if len(dirEntries) < 1 {
		err = errors.New(fmt.Sprintf("未读取到有效的像素信息:%s", err))
		return
	}
	// 读取第一个图标数据
	_, _ = f.Seek(int64(dirEntries[0].Offset), 0)
	icoData := make([]byte, dirEntries[0].Size)
	if _, err = f.Read(icoData); err != nil {
		err = errors.New(fmt.Sprintf("无法读取ICO图标数据:%s", err))
		return
	}
	// 这个png图片有可能是可以打开但是转换为base64后浏览器无法识别的情况
	// 可能是看图软件做了兼容，具体情况后面在研究
	pngImg := "./" + uuid.NewV4().String() + ".png"
	pngFile, err := os.Create(pngImg)
	if err != nil {
		err = errors.New(fmt.Sprintf("无法创建PNG文件:%s", err))
		return
	}
	defer pngFile.Close()
	if _, err = pngFile.Write(icoData); err != nil {
		err = errors.New(fmt.Sprintf("无法写入PNG文件:%s", err))
		return
	}
	str = "data:image/png;base64," + base64.StdEncoding.EncodeToString(icoData)
	return
}

// getIpMac 获取当前活跃ipv4地址和其对应的网卡地址
func getIpMac() (ip, mac string, err error) {
	var (
		p psnet.InterfaceStatList
	)
	if ip, err = getOutBoundIP(); err != nil {
		return
	}
	if p, err = psnet.Interfaces(); err != nil {
		return
	}
	for _, item := range p {
		if len(item.Addrs) > 0 {
			for _, inter := range item.Addrs {
				ipv4 := inter.Addr
				ipv4 = ipv4[:strings.LastIndex(ipv4, "/")]
				if ipv4 == ip {
					mac = item.HardwareAddr
					mac = strings.ReplaceAll(mac, ":", "-")
				}
			}
		}
	}
	return
}

func getOutBoundIP() (ip string, err error) {
	var (
		conn net.Conn
	)
	conn, err = net.Dial("udp", "8.8.8.8:9999")
	if err != nil {
		return "", err
	}
	localAdd := conn.LocalAddr()
	ip = strings.Split(localAdd.String(), ":")[0]
	return ip, nil
}

// getRdpPort 通过注册表获取rdp端口信息
func getRdpPort() (port uint, err error) {
	var (
		key  registry.Key
		pval uint64
	)
	if key, err = registry.OpenKey(registry.LOCAL_MACHINE, regQueryForRdpPort, registry.ALL_ACCESS); err != nil {
		return
	}
	if pval, _, err = key.GetIntegerValue("PortNumber"); err != nil {
		return
	}

	return uint(pval), err
}

// batchCreateUsers 批量创建 remote app 账户
func batchCreateUsers(str string) (err error) {
	us := strings.Split(str, "-")
	ud := NewUserDll()
	for i := range us {
		_, _ = ud.AddUser(us[i], "qwer1234!")
		_, _ = ud.AddUserToGroup(us[i], "Remote Desktop Users")
		_ = os.MkdirAll(fmt.Sprintf("E:\\user\\%s", us[i]), 777)
		_, _, _, _ = cmdutil.RunCommandArgs([]string{"C:\\Windows\\System32\\Cacls.exe", fmt.Sprintf("E:\\user\\%s", us[i]), "/E", "/G", fmt.Sprintf("%s:F", us[i])}, cmdutil.CommandOption{})
		if err != nil {
			return errors.New(fmt.Sprintf("调整目录权限时发生错误:%s", err))
		}
	}
	return nil
}
